package com.fzk.server;

import com.fzk.env.MyEnv;
import com.fzk.env.conf.ServerConf;
import com.fzk.router.MyCtx;
import com.fzk.utils.MyHmac;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedFile;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

import static io.netty.handler.codec.http.HttpVersion.HTTP_1_0;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;

/**
 * 静态资源
 *
 * @author fzk
 * @datetime 2023-02-14 00:58:40
 */
public class StaticResourcesHandler {
    public static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
    public static final String HTTP_DATE_GMT_TIMEZONE = "GMT";
    private final static ServerConf serverConf = MyEnv.getConf("server.properties", ServerConf.class);

    /**
     * 输出静态资源
     * 条件get304判断
     */
    public static void serve(MyCtx myctx, FullHttpRequest request, Path path) throws IOException, ParseException {
        ChannelHandlerContext ctx = myctx.getChannelCtx();
        String uri = request.uri();
        // 1.首先检查方法为GET/POST
        if (!HttpMethod.GET.equals(request.method()) && !HttpMethod.POST.equals(request.method())) {
            HttpResponseUtil.sendError(ctx, HttpUtil.isKeepAlive(request), HttpResponseStatus.METHOD_NOT_ALLOWED);
            return;
        }

        // 2.文件/目录判断
        // 2.1 文件不存在或不可见
        if (Files.notExists(path) || Files.isHidden(path)) {
            HttpResponseUtil.sendError(ctx, HttpUtil.isKeepAlive(request), HttpResponseStatus.NOT_FOUND);
            return;
        }
        // 2.2 文件目录
        if (Files.isDirectory(path)) {
            if (uri.endsWith("/")) {// 如果是目录访问请求，则将目录包含的文件列表输出
                HttpResponseUtil.sendReqRedirect(ctx, request, "/fs/api/dirFileList?dir=" + path);
            } else {
                HttpResponseUtil.sendReqRedirect(ctx, request, uri + "/");
            }
            return;
        }
        if (!Files.isRegularFile(path)) {
            HttpResponseUtil.sendError(ctx, HttpUtil.isKeepAlive(request), HttpResponseStatus.FORBIDDEN);
            return;
        }

        // 3.条件GET判断：先判断last-modified再比较etag
        if (request.headers().contains(HttpHeaderNames.IF_MODIFIED_SINCE) &&
                notModifiedSince(path, request.headers().get(HttpHeaderNames.IF_MODIFIED_SINCE))) {
            // 是否需要比较etag
            if (!request.headers().contains(HttpHeaderNames.IF_NONE_MATCH) ||
                    isMatchEtag(path, request.headers().get(HttpHeaderNames.IF_NONE_MATCH))) {
                sendNotModified(ctx, HttpUtil.isKeepAlive(request));
                return;
            }
        }

        DefaultHttpResponse response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.OK);
        // 4.支持http范围请求：一般用于断点续传下载
        long size = Files.size(path);// 文件大小
        long offset = 0, count = size;// 文件偏移量和需要传输的字节数
        String range = request.headers().get(HttpHeaderNames.RANGE);
        if (range != null && range.startsWith("bytes=")) {
            // 判断是否存在If-Range请求头，它表示客户端询问资源是否发生修改
            boolean notModified = true;
            if (request.headers().contains(HttpHeaderNames.IF_RANGE)) {
                // 断点续传，中断之后重新请求资源片段之前，必须确保该资源没有改变，两种格式:
                // If-Range: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
                // If-Range: <etag>
                String ifRange = request.headers().get(HttpHeaderNames.IF_RANGE);
                if (ifRange.contains(HTTP_DATE_GMT_TIMEZONE)) {
                    notModified = notModifiedSince(path, ifRange);
                } else {// 比较etag
                    notModified = isMatchEtag(path, ifRange);
                }
            }
            if (notModified) {
                String[] splits = range.substring(6).split("-");
                if (splits.length == 2 && splits[0].length() > 0 && splits[1].length() > 0) {
                    // 数据范围是闭区间
                    long start = Long.parseLong(splits[0]), end = Long.parseLong(splits[1]);
                    if (start < 0 || start > end) {// 请求头有问题
                        HttpResponseUtil.sendError(ctx, HttpUtil.isKeepAlive(request), HttpResponseStatus.BAD_REQUEST,
                                String.format("http范围请求, 参数错误range: %s", range));
                        return;
                    }
                    if (end >= size) {// 请求范围越界，416(Requested Range Not Satisfiable)
                        HttpResponseUtil.sendError(ctx, HttpUtil.isKeepAlive(request), HttpResponseStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
                                String.format("http范围请求, range: %s, 超过了文件大小: %d", range, size));
                        return;
                    }
                    offset = start;
                    count = end - start + 1;// 闭区间
                    response.setStatus(HttpResponseStatus.PARTIAL_CONTENT);// 范围响应206
                    response.headers().set(HttpHeaderNames.ACCEPT_RANGES, "bytes");
                    response.headers().set(HttpHeaderNames.CONTENT_RANGE, String.format("bytes %d-%d/%d", start, end, size));
                }
            }
        }

        // 5.输出文件
        // 5.1 设置content-length和content-type和content-disposition
        response.headers().set(HttpHeaderNames.CONTENT_LENGTH, count);
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, MyHttpUtil.getContentType(path));
        if (MyHttpUtil.isAttachment(path)) {// 如果不能浏览器直接展示，指定文件名下载到本地
            response.headers().set(HttpHeaderNames.CONTENT_DISPOSITION, "attachment; filename=" + URLEncoder.encode(path.getFileName().toString(), StandardCharsets.UTF_8));
        } else {
            response.headers().set(HttpHeaderNames.CONTENT_DISPOSITION, "inline; filename=" + URLEncoder.encode(path.getFileName().toString(), StandardCharsets.UTF_8));
        }

        // 5.2 设置文件缓存
        setDateAndCacheHeaders(response, path.toFile());
        // 5.3 设置connection响应头
        if (!HttpUtil.isKeepAlive(request)) {
            HttpUtil.setKeepAlive(response, false);
        } else if (request.protocolVersion().equals(HTTP_1_0)) {// 这个SB 1.0版本最好手动设置为长连接
            response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
        }
        // 5.4 输出响应行/头
        ctx.write(response);
        // 5.5 输出响应内容
        ChannelFuture sendFileFuture, lastContentFuture;
        if (ctx.pipeline().get(SslHandler.class) == null) {// 默认走这个, 会调用fileChannel#transferTo()零拷贝
            sendFileFuture = ctx.write(new DefaultFileRegion(path.toFile(), offset, count), ctx.newProgressivePromise());
            // Write the end marker.
            lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
        } else {
            sendFileFuture =
                    ctx.writeAndFlush(new HttpChunkedInput(new ChunkedFile(new RandomAccessFile(path.toFile(), "r"), offset, count, 8192)),
                            ctx.newProgressivePromise());
            // HttpChunkedInput will write the end marker (LastHttpContent) for us.
            lastContentFuture = sendFileFuture;
        }

        sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
            @Override // 操作进行时调用，显示进度条?
            public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) {
                if (total < 0) { // total unknown
                    System.err.println(future.channel() + " Transfer progress: " + progress);
                } else {
                    System.err.println(future.channel() + " Transfer progress: " + progress + " / " + total);
                }
            }

            @Override // 操作完成时调用
            public void operationComplete(ChannelProgressiveFuture future) {
                myctx.info("文件下载完成, path: " + path);
//                Logger.info("文件下载完成, path: " + path);
            }
        });
        // 6.判断是否需要及时关闭连接
        if (!HttpUtil.isKeepAlive(request)) {
            // Close the connection when the whole content is written out.
            lastContentFuture.addListener(ChannelFutureListener.CLOSE);
        }
    }

    private static boolean notModifiedSince(Path path, String ifModifiedSince) throws ParseException {
        SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);// 这里必须是US时区
        Date ifModifiedSinceDate = dateFormatter.parse(ifModifiedSince);
        // 比较last-modified判断文件是否发生修改
        // Only compare up to the second because the datetime format we send to the client
        // does not have milliseconds
        long ifModifiedSinceDateSeconds = ifModifiedSinceDate.getTime() / 1000;
        long fileLastModifiedSeconds = path.toFile().lastModified() / 1000;
        return ifModifiedSinceDateSeconds == fileLastModifiedSeconds;
    }

    private static boolean isMatchEtag(Path path, String etag) throws IOException {
        // 比较Etag，即以md5值比较文件内容是否发生变化
        // 判断是否开启etag校验：因为大文件计算hash开销较大
        if (serverConf.isHttpCacheEtagValid()) {
            return etag.equals(MyHmac.getFileMD5(path)); // 以md5值作为etag
        } else {// 没开校验但是又传递了etag则校验默认etag
            return etag.equals(serverConf.getHttpDefaultEtag());
        }
    }

    // 文件缓存设置
    private static void setDateAndCacheHeaders(HttpResponse response, File fileToCache) throws IOException {
        SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
        dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));

        // Date header
        Calendar time = new GregorianCalendar();
        response.headers().set(HttpHeaderNames.DATE, dateFormatter.format(time.getTime()));

        // Add cache headers
        time.add(Calendar.SECOND, serverConf.getHttpCacheTime());
        response.headers().set(HttpHeaderNames.EXPIRES, dateFormatter.format(time.getTime()));
        //response.headers().set(HttpHeaderNames.CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS);
        response.headers().set(HttpHeaderNames.CACHE_CONTROL, "max-age=" + serverConf.getHttpCacheTime());
        response.headers().set(HttpHeaderNames.LAST_MODIFIED, dateFormatter.format(new Date(fileToCache.lastModified())));
        // 设置etag
        String etag = serverConf.getHttpDefaultEtag();
        // 开启etag校验才去计算hash值，因为大文件计算hash开销很大
        if (serverConf.isHttpCacheEtagValid()) {
            etag = MyHmac.getFileMD5(fileToCache.toPath());
        }
        response.headers().set(HttpHeaderNames.ETAG, etag);
    }

    // 当文件时间戳与浏览器发送的相同时，发送“304 Not Modified”
    private static void sendNotModified(ChannelHandlerContext ctx, boolean keepAlive) {
        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.NOT_MODIFIED, Unpooled.EMPTY_BUFFER);
        setDateHeader(response);

        HttpResponseUtil.sendAndCleanupConnection(ctx, keepAlive, response);
    }

    // 设置 HTTP 响应的日期标头
    private static void setDateHeader(FullHttpResponse response) {
        SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
        dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));

        Calendar time = new GregorianCalendar();
        response.headers().set(HttpHeaderNames.DATE, dateFormatter.format(time.getTime()));
    }
}
